#include "item/item.h"
#include "item/handle.h"
#include "OldGraphicsView/Palette.h"
#include "xdl/symbol.h"

#include <QPen>
#include <QBrush>

#include <QtMath>
#include <QVector2D>

namespace SymbolEditor
{

    GraphicsItem::GraphicsItem(GraphicsItem *parent):
        QGraphicsItem(parent),
        m_isXMirrored(false),
        m_isYMirrored(false)
    {
        setLineStyle(SolidLine);
        setLineWidth(MediumLine);
        setLineColor(PrimaryContent);
        setFillStyle(SolidFill);
        setFillColor(Background);
        setFlags(ItemIsSelectable | ItemIsMovable);
    }

    GraphicsItem::~GraphicsItem()
    {
    }

    int GraphicsItem::handleCount() const
    {
        return m_idToHandle.count();
    }


    void GraphicsItem::cloneTo(GraphicsItem *dst)
    {        
        dst->setLineStyle(lineStyle());
        dst->setLineWidth(lineWidth());
        dst->setLineColor(lineColor());
        dst->setFillStyle(fillStyle());
        dst->setFillColor(fillColor());
        dst->setEnabled(isEnabled());
        dst->setFlags(flags());
        dst->setOpacity(opacity());
        dst->setPos(pos());
        dst->setRotation(rotation());
        dst->setSelected(isSelected());
        dst->setScale(scale());
        dst->setVisible(isVisible());
        dst->setZValue(zValue());
        dst->setTransform(transform());
        // Handles don't have to be copied as their creation is controlled by the item itself
    }

    // From Qt qgraphicsitem.cpp: qt_graphicsItem_shapeFromPath()
    QPainterPath GraphicsItem::shapeFromPath(const QPainterPath &path, const QPen &pen)
    {
        // We unfortunately need this hack as QPainterPathStroker will set a width of 1.0
        // if we pass a value of 0.0 to QPainterPathStroker::setWidth()
        const qreal penWidthZero = qreal(0.00000001);

        if (path == QPainterPath() || pen == Qt::NoPen)
        {
            return path;
        }
        QPainterPathStroker ps;
        ps.setCapStyle(pen.capStyle());
        if (pen.widthF() <= 0.0)
        {
            ps.setWidth(penWidthZero);
        }
        else
        {
            ps.setWidth(pen.widthF());
        }
        ps.setJoinStyle(pen.joinStyle());
        ps.setMiterLimit(pen.miterLimit());
        QPainterPath p = ps.createStroke(path);
        p.addPath(path); // not needed for open path (eg, wire)
        return p;
    }

    void GraphicsItem::updateMirroringTransform()
    {
        qreal m11 = m_isXMirrored ? -1 : 1;
        qreal m22 = m_isYMirrored ? -1 : 1;
        setTransform(QTransform(m11, 0,   0,
                                0,   m22, 0,
                                0,   0,   1));
    }

    void GraphicsItem::addAbstractHandle(Handle *handle)
    {
        // could be done by abstracthandle
        addObservedItem(handle);

        m_handleToId[handle] = handle->handleId();
        m_idToHandle[handle->handleId()] = handle;
    }

    Handle *GraphicsItem::handleAt(int idx)
    {
        Q_ASSERT(idx < handleCount());
        return m_idToHandle[idx];
    }

    void GraphicsItem::setLineStyle(LineStyle style)
    {
        if (m_lineStyle == style)
        {
            return;
        }

        m_lineStyle = style;
        update();
    }

    LineStyle GraphicsItem::lineStyle() const
    {
        return m_lineStyle;
    }

    void GraphicsItem::setLineWidth(LineWidth width)
    {
        if (m_lineWidth == width)
        {
            return;
        }

        prepareGeometryChange();
        m_lineWidth = width;
        m_boundingRect = QRectF();
        update();
    }

    LineWidth GraphicsItem::lineWidth() const
    {
        return m_lineWidth;
    }

    void GraphicsItem::setLineColor(Color color)
    {
        if (m_lineColor == color)
        {
            return;
        }

        m_lineColor = color;
        update();
    }

    Color GraphicsItem::lineColor() const
    {
        return m_lineColor;
    }

    void GraphicsItem::setFillStyle(FillStyle style)
    {
        if (m_fillStyle == style)
        {
            return;
        }

        m_fillStyle = style;
        update();
    }

    FillStyle GraphicsItem::fillStyle() const
    {
        return m_fillStyle;
    }

    void GraphicsItem::setFillColor(Color color)
    {
        if (m_fillColor == color)
        {
            return;
        }

        m_fillColor = color;
        update();
    }

    Color GraphicsItem::fillColor() const
    {
        return m_fillColor;
    }

    bool GraphicsItem::isXMirrored() const
    {
        return m_isXMirrored;
    }

    bool GraphicsItem::isYMirrored() const
    {
        return m_isYMirrored;
    }

    // Return a list of hot spots in item's coordinate
    QList<QPointF> GraphicsItem::hotSpots() const
    {
        QList<QPointF> points;
        for (Handle *handle : m_handleToId.keys())
        {
            Q_ASSERT(handle->parentItem() == this);
            points.append(handle->pos());
        }
        return points;
    }

    QList<QPointF> GraphicsItem::endPoints() const
    {
        return QList<QPointF>();
    }

    QList<QPointF> GraphicsItem::midPoints() const
    {
        return QList<QPointF>();
    }

    QList<QPointF> GraphicsItem::centerPoints() const
    {
        return QList<QPointF>();
    }

    QList<QPointF> GraphicsItem::nearestPoints(QPointF pos) const
    {
        Q_UNUSED(pos);
        return QList<QPointF>();
    }

    QList<PropertyId> GraphicsItem::propertyIdList() const
    {
        return QList<PropertyId>() << PositionProperty
                                   << OpacityProperty
                                   << RotationProperty
                                   << XMirroredProperty
                                   << YMirroredProperty
                                   << LockedProperty
                                   << VisibilityProperty
                                   << LineStyleProperty
                                   << LineWidthProperty
                                   << LineColorProperty
                                   << FillStyleProperty
                                   << FillColorProperty;
    }

    void GraphicsItem::setProperty(PropertyId id, const QVariant &value)
    {
        switch (id)
        {
            case PositionProperty:
                setPos(value.toPointF());
                break;
            case OpacityProperty:
                setOpacity(value.toReal()/100.0);
                break;
            case RotationProperty:
                setRotation(value.toReal());
                break;
            case XMirroredProperty:
                setXMirrored(value.toBool());
                break;
            case YMirroredProperty:
                setYMirrored(value.toBool());
                break;
            case LockedProperty:
                setEnabled(!value.toBool());
                break;
            case VisibilityProperty:
                setVisible(value.toBool());
                break;
            case LineStyleProperty:
                Q_ASSERT(value.canConvert<LineStyle>());
                setLineStyle(value.value<LineStyle>());
                break;
            case LineWidthProperty:
                Q_ASSERT(value.canConvert<LineWidth>());
                setLineWidth(value.value<LineWidth>());
                break;
            case LineColorProperty:
                Q_ASSERT(value.canConvert<Color>());
                setLineColor(value.value<Color>());
                break;
            case FillStyleProperty:
                Q_ASSERT(value.canConvert<FillStyle>());
                setFillStyle(value.value<FillStyle>());
                break;
            case FillColorProperty:
                Q_ASSERT(value.canConvert<Color>());
                setFillColor(value.value<Color>());
                break;
            default:
                break;
        }
    }

    QVariant GraphicsItem::property(PropertyId id) const
    {
        switch (id)
        {
            case PositionProperty:
                return pos();
            case OpacityProperty:
                return opacity() * 100.0;
            case RotationProperty:
                return rotation();
            case XMirroredProperty:
                return isXMirrored();
            case YMirroredProperty:
                return isYMirrored();
            case LockedProperty:
                return !isEnabled();
            case VisibilityProperty:
                return isVisible();
            case LineStyleProperty:
                return QVariant::fromValue(lineStyle());
            case LineWidthProperty:
                return QVariant::fromValue(lineWidth());
            case LineColorProperty:
                return QVariant::fromValue(lineColor());
            case FillStyleProperty:
                return QVariant::fromValue(fillStyle());
            case FillColorProperty:
                return QVariant::fromValue(fillColor());
            default:
                return QVariant();
        }
    }

    void GraphicsItem::setProperties(QMap<PropertyId, QVariant> properties)
    {
        for (auto id : properties.keys())
        {
            setProperty(id, properties.value(id));
        }
    }

    QMap<PropertyId, QVariant> GraphicsItem::properties() const
    {
        QMap<PropertyId, QVariant> result;
        for (auto id : propertyIdList())
        {
            result.insert(id, property(id));
        }
        return result;
    }

    void GraphicsItem::setPalette(LeGraphicsView::Palette palette)
    {
        m_palette = palette;
        for (auto handle: m_handleToId.keys())
        {
            handle->setPalette(m_palette);
        }
        update();
    }

    LeGraphicsView::Palette GraphicsItem::palette() const
    {
        return m_palette;
    }

    QPen GraphicsItem::pen() const
    {
        QPen pen;
        switch (m_lineStyle)
        {
            case SymbolEditor::NoLine:
                pen.setStyle(Qt::NoPen);
                break;
            case SymbolEditor::SolidLine:
                pen.setStyle(Qt::SolidLine);
                break;
            case SymbolEditor::DotLine:
                pen.setStyle(Qt::DotLine);
                break;
            case SymbolEditor::DashLine:
                pen.setStyle(Qt::DashLine);
                break;
            case SymbolEditor::DashDotLine:
                pen.setStyle(Qt::DashDotLine);
                break;
            case SymbolEditor::DashDotDotLine:
                pen.setStyle(Qt::DashDotDotLine);
                break;
        }
        switch (m_lineWidth)
        {
            case SymbolEditor::ThinestLine:
                pen.setWidthF(0.13);
                break;
            case SymbolEditor::ThinerLine:
                pen.setWidthF(0.18);
                break;
            case SymbolEditor::ThinLine:
                pen.setWidthF(0.25);
                break;
            case SymbolEditor::SlightlyThinLine:
                pen.setWidthF(0.35);
                break;
            case SymbolEditor::MediumLine:
                pen.setWidthF(0.5);
                break;
            case SymbolEditor::SlightlyThickLine:
                pen.setWidthF(0.7);
                break;
            case SymbolEditor::ThickLine:
                pen.setWidthF(1.0);
                break;
            case SymbolEditor::ThickerLine:
                pen.setWidthF(1.4);
                break;
            case SymbolEditor::ThickestLine:
                pen.setWidthF(2.0);
                break;
        }
        pen.setColor(m_palette.color(LeGraphicsView::Palette::ColorId(m_lineColor)));
        return pen;
    }

    QBrush GraphicsItem::brush() const
    {
        QBrush brush;
        switch (m_fillStyle)
        {
            case SymbolEditor::NoFill:
                brush.setStyle(Qt::NoBrush);
                break;
            case SymbolEditor::SolidFill:
                brush.setStyle(Qt::SolidPattern);
                break;
        }
        brush.setColor(m_palette.color(LeGraphicsView::Palette::ColorId(m_fillColor)));
        return brush;
    }

    void GraphicsItem::setXMirrored(bool mirrored)
    {
        if (m_isXMirrored == mirrored)
        {
            return;
        }
        m_isXMirrored = mirrored;
        updateMirroringTransform();
    }

    void GraphicsItem::setYMirrored(bool mirrored)
    {
        if (m_isYMirrored == mirrored)
        {
            return;
        }
        m_isYMirrored = mirrored;
        updateMirroringTransform();
    }

    Handle *GraphicsItem::addHandle(int id, Handle::Role role,
                            Handle::Shape shape, const QPointF &pos)
    {
        auto *handle = new Handle(this);

        Q_ASSERT(!m_idToHandle.keys().contains(id));

        handle->setHandleId(id);
        handle->setHandleRole(role);
        handle->setHandleShape(shape);
        handle->setPos(pos);

        addAbstractHandle(handle);

        return handle;
    }

    void GraphicsItem::removeHandle(int id)
    {
        Q_ASSERT(m_idToHandle.keys().contains(id));

        removeHandle(m_idToHandle[id]);
    }

    void GraphicsItem::removeHandle(Handle *handle)
    {
        Q_ASSERT(m_handleToId.keys().contains(handle));

        blockItemNotification();
        int id = m_handleToId[handle];
        m_handleToId.remove(handle);
        m_idToHandle.remove(id);
        removeObservedItem(handle);
        handle->setParentItem(nullptr);
        delete handle;
        unblockItemNotification();
    }

    void GraphicsItem::removeAllHandles()
    {
        blockItemNotification();
        for (Handle *handle : m_handleToId.keys())
        {
            removeObservedItem(handle);
            handle->setParentItem(nullptr);
            delete handle;
        }
        unblockItemNotification();
        m_handleToId.clear();
        m_idToHandle.clear();
    }

    Handle *GraphicsItem::handle(int id) const
    {
        Q_ASSERT(id < handleCount());
        return m_idToHandle[id];
    }


    QPointF GraphicsItem::nearestPoint(const QLineF &line, const QPointF &point)
    {
        QVector2D AP(point - line.p1());
        QVector2D AB(line.p2() - line.p1());
        auto magnitude = AB.lengthSquared();
        auto product = QVector2D::dotProduct(AP, AB);
        auto distance = product / magnitude;

        if (distance < 0)
        {
            return line.p1();
        }

        if (distance > 1)
        {
            return  line.p2();
        }

        return line.pointAt(distance);
    }

    QPointF GraphicsItem::nearestPoint(const QPointF &center, qreal radius, const QPointF &point)
    {
        QLineF line(center, point);
        line.setLength(radius);
        return line.p2();
    }

    QPointF GraphicsItem::nearestPoint(const QPointF &center, qreal radius,
                                       qreal startAngle, qreal spanAngle,
                                       const QPointF &point)
    {
        qreal stopAngle = fmod(startAngle + spanAngle, 360.0);
        QList<QPointF> candidates;
        QLineF line(center, point);
        line.setLength(radius);

        if (startAngle < line.angle() && line.angle() < stopAngle)
        {
            candidates << line.p2();
        }

        line.setAngle(startAngle);
        candidates << nearestPoint(line, point);
        line.setAngle(stopAngle);
        candidates << nearestPoint(line, point);

        QPointF nearest(qInf(), qInf());
        for (auto candidate: candidates)
        {
            if ((point - candidate).manhattanLength() < (point - nearest).manhattanLength())
            {
                nearest = candidate;
            }
        }
        return nearest;
    }

    QPointF GraphicsItem::nearestPoint(const QList<QPointF> &candidates, const QPointF &point)
    {
        qreal shortest = qInf();
        QPointF nearest;
        for (auto candidate: candidates)
        {
            auto vector = point - candidate;
            auto distance = vector.manhattanLength();
            if (distance < shortest)
            {
                nearest = candidate;
                shortest = distance;
            }
        }
        return nearest;
    }
}
